// ##########################################################################
// #                                                                        #
// #                              CLOUDCOMPARE                              #
// #                                                                        #
// #  This program is free software; you can redistribute it and/or modify  #
// #  it under the terms of the GNU General Public License as published by  #
// #  the Free Software Foundation; version 2 or later of the License.      #
// #                                                                        #
// #  This program is distributed in the hope that it will be useful,       #
// #  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
// #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          #
// #  GNU General Public License for more details.                          #
// #                                                                        #
// #          COPYRIGHT: EDF R&D / TELECOM ParisTech (ENST-TSI)             #
// #                                                                        #
// ##########################################################################

#include "SinusxFilter.h"

// qCC_db
#include <ccLog.h>
#include <ccPointCloud.h>
#include <ccPolyline.h>

// Qt
#include <QDialog>
#include <QFile>
#include <QTextStream>

// System
#include <cstring>

SinusxFilter::SinusxFilter()
    : FileIOFilter({"_Sinusx Filter",
                    DEFAULT_PRIORITY, // priority
                    QStringList{"sx", "sinusx"},
                    "sx",
                    QStringList{"Sinusx curve (*.sx)"},
                    QStringList{"Sinusx curve (*.sx)"},
                    Import | Export})
{
}

bool SinusxFilter::canSave(CC_CLASS_ENUM type, bool& multiple, bool& exclusive) const
{
	if (type == CC_TYPES::POLY_LINE)
	{
		multiple  = true;
		exclusive = true;
		return true;
	}
	return false;
}

QString MakeSinusxName(QString name)
{
	// no space characters
	name.replace(' ', '_');

	return name;
}

CC_FILE_ERROR SinusxFilter::saveToFile(ccHObject* entity, const QString& filename, const SaveParameters& parameters)
{
	if (!entity || filename.isEmpty())
		return CC_FERR_BAD_ARGUMENT;

	// look for polylines only
	std::vector<ccPolyline*> profiles;
	try
	{
		if (entity->isA(CC_TYPES::POLY_LINE))
		{
			profiles.push_back(static_cast<ccPolyline*>(entity));
		}
		else if (entity->isA(CC_TYPES::HIERARCHY_OBJECT))
		{
			for (unsigned i = 0; i < entity->getChildrenNumber(); ++i)
				if (entity->getChild(i) && entity->getChild(i)->isA(CC_TYPES::POLY_LINE))
					profiles.push_back(static_cast<ccPolyline*>(entity->getChild(i)));
		}
	}
	catch (const std::bad_alloc&)
	{
		return CC_FERR_NOT_ENOUGH_MEMORY;
	}

	if (profiles.empty())
		return CC_FERR_NO_SAVE;

	// open ASCII file for writing
	QFile file(filename);
	if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
		return CC_FERR_WRITING;

	QTextStream      outFile(&file);
	static const int s_precision = 12;
	outFile.setRealNumberNotation(QTextStream::FixedNotation);
	outFile.setRealNumberPrecision(s_precision);

	CC_FILE_ERROR result = CC_FERR_NO_SAVE;

	// write header
	outFile << "C Generated by CloudCompare" << endl;

	// for each profile
	for (size_t i = 0; i < profiles.size(); ++i)
	{
		ccPolyline* poly      = profiles[i];
		unsigned    vertCount = poly ? poly->size() : 0;
		if (vertCount < 2)
		{
			// invalid size
			ccLog::Warning(QString("[Sinusx] Polyline '%1' does not have enough vertices")
			                   .arg(poly ? poly->getName() : QStringLiteral("unnamed")));
			continue;
		}

		bool is2D = poly->is2DMode();

		int upDir = 2;
		if (is2D && poly->hasMetaData(ccPolyline::MetaKeyUpDir()))
		{
			bool ok;
			upDir = poly->getMetaData(ccPolyline::MetaKeyUpDir()).toInt(&ok);
			if (!ok)
				upDir = 2; // restore default value
		}

		// new block
		outFile << "B S" << endl;                                            // B + curve type (S = set of 3D points)
		outFile << "CN " << poly->getName() << endl;                         // CN + name
		outFile << "CP 1 " << (poly->isClosed() ? 1 : 0) << endl;            // CP + isLinked + isClosed
		outFile << "CP " << (upDir == 2 ? 0 : (upDir == 1 ? 2 : 1)) << endl; // base plane: 0 = (XY), 1 = (YZ), 2 = (ZX)

		for (unsigned j = 0; j < vertCount; ++j)
		{
			const CCVector3* P  = poly->getPoint(j);
			CCVector3d       Pg = poly->toGlobal3d(*P);

			for (unsigned k = 0; k < 3; ++k)
			{
				outFile << " ";
				if (P->u[k] >= 0)
					outFile << "+";
				outFile << QString::number(Pg.u[k], 'E', s_precision);
			}
			outFile << " A" << endl;
		}

		result = CC_FERR_NO_ERROR;
	}

	file.close();

	return result;
}

enum CurveType
{
	INVALID = -1,
	CUREV_S = 0,
	CURVE_P = 1,
	CURVE_N = 2,
	CURVE_C = 3
};
const QChar SHORTCUT[] = {'S', 'P', 'N', 'C'};

CC_FILE_ERROR SinusxFilter::loadFile(const QString& filename, ccHObject& container, LoadParameters& parameters)
{
	// open file
	QFile file(filename);
	if (!file.open(QFile::ReadOnly))
		return CC_FERR_READING;
	QTextStream stream(&file);

	QString       currentLine("C");
	ccPolyline*   currentPoly     = nullptr;
	ccPointCloud* currentVertices = nullptr;
	unsigned      lineNumber      = 0;
	CurveType     curveType       = INVALID;
	unsigned      cpIndex         = 0;
	CC_FILE_ERROR result          = CC_FERR_NO_ERROR;
	CCVector3d    Pshift(0, 0, 0);
	bool          firstVertex = true;

	while (!currentLine.isEmpty() && file.error() == QFile::NoError)
	{
		currentLine = stream.readLine();
		++lineNumber;

		if (currentLine.startsWith("C "))
		{
			// ignore comments
			continue;
		}
		else if (currentLine.startsWith("B"))
		{
			// new block
			if (currentPoly)
			{
				if (currentVertices
				    && currentVertices->size() != 0
				    && currentVertices->resize(currentVertices->size())
				    && currentPoly->addPointIndex(0, currentVertices->size()))
				{
					container.addChild(currentPoly);
				}
				else
				{
					delete currentPoly;
				}
				currentPoly     = nullptr;
				currentVertices = nullptr;
			}
			// read type
			QStringList tokens = currentLine.simplified().split(QChar(' '), QString::SkipEmptyParts);
			if (tokens.size() < 2 || tokens[1].length() > 1)
			{
				ccLog::Warning(QString("[SinusX] Line %1 is corrupted").arg(lineNumber));
				result = CC_FERR_MALFORMED_FILE;
				continue;
			}
			QChar curveTypeChar = tokens[1].at(0);
			curveType           = INVALID;
			if (curveTypeChar == SHORTCUT[CUREV_S])
				curveType = CUREV_S;
			else if (curveTypeChar == SHORTCUT[CURVE_P])
				curveType = CURVE_P;
			else if (curveTypeChar == SHORTCUT[CURVE_N])
				curveType = CURVE_N;
			else if (curveTypeChar == SHORTCUT[CURVE_C])
				curveType = CURVE_C;

			if (curveType == INVALID)
			{
				ccLog::Warning(QString("[SinusX] Unhandled curve type '%1' on line '%2'!").arg(curveTypeChar).arg(lineNumber));
				result = CC_FERR_MALFORMED_FILE;
				continue;
			}

			// TODO: what about the local coordinate system and scale?! (7 last values)
			if (tokens.size() > 7)
			{
			}

			// block is ready
			currentVertices = new ccPointCloud("vertices");
			currentPoly     = new ccPolyline(currentVertices);
			currentPoly->addChild(currentVertices);
			currentVertices->setEnabled(false);
			cpIndex = 0;
		}
		else if (currentPoly)
		{
			if (currentLine.startsWith("CN"))
			{
				if (currentLine.length() > 3)
				{
					QString name = currentLine.right(currentLine.length() - 3);
					currentPoly->setName(name);
				}
			}
			else if (currentLine.startsWith("CP"))
			{
				QStringList tokens = currentLine.simplified().split(QChar(' '), QString::SkipEmptyParts);

				switch (cpIndex)
				{
				case 0: // first 'CP' line
				{
					// expected: CP + 'connected' + 'closed' flags
					bool ok = (tokens.size() == 3);
					if (ok)
					{
						bool ok1         = true;
						bool ok2         = true;
						int  isConnected = tokens[1].toInt(&ok1);
						int  isClosed    = tokens[2].toInt(&ok2);
						ok               = ok1 && ok2;
						if (ok)
						{
							if (isConnected == 0)
							{
								// points are not connected?!
								//--> we simply hide the polyline and display its vertices
								currentPoly->setVisible(false);
								currentVertices->setEnabled(true);
							}
							currentPoly->setClosed(isClosed != 0);
						}
					}
					if (!ok)
					{
						ccLog::Warning(QString("[SinusX] Line %1 is corrupted (expected: 'CP connected_flag closed_flag')").arg(lineNumber));
						result = CC_FERR_MALFORMED_FILE;
						continue;
					}
					++cpIndex;
				}
				break;

				case 1: // second 'CP' line
				{
					if (curveType == CUREV_S)
					{
						++cpIndex;
						// no break: we go directly to the next case (cpIndex = 2)
					}
					else if (curveType == CURVE_P)
					{
						// nothing particular for profiles (they are not handled in the same way in CC!)
						++cpIndex;
						break;
					}
					else if (curveType == CURVE_N)
					{
						// expected: CP + const_altitude
						bool ok = (tokens.size() == 2);
						if (ok)
						{
							double z = tokens[1].toDouble(&ok);
							if (ok)
								currentPoly->setMetaData(ccPolyline::MetaKeyConstAltitude(), QVariant(z));
						}
						if (!ok)
						{
							ccLog::Warning(QString("[SinusX] Line %1 is corrupted (expected: 'CP const_altitude')").arg(lineNumber));
							result = CC_FERR_MALFORMED_FILE;
							continue;
						}
						++cpIndex;
						break;
					}
					else if (curveType == CURVE_C)
					{
						// skip the next 16 values
						int skipped = tokens.size() - 1; // all but the 'CP' keyword
						while (skipped < 16 && !currentLine.isEmpty() && file.error() == QFile::NoError)
						{
							currentLine = stream.readLine();
							++lineNumber;
							tokens = currentLine.simplified().split(QChar(' '), QString::SkipEmptyParts);
							skipped += tokens.size();
						}
						assert(skipped == 16); // no more than 16 normally!
						++cpIndex;
						break;
					}
					else
					{
						assert(false);
						++cpIndex;
						break;
					}
				}

				case 2:
				{
					// CP + base plane: 0 = (XY), 1 = (YZ), 2 = (ZX)
					bool ok = (tokens.size() == 2);
					if (ok)
					{
						int   vertDir       = 2;
						QChar basePlaneChar = tokens[1].at(0);
						if (basePlaneChar == '0')
							vertDir = 2;
						else if (basePlaneChar == '1')
							vertDir = 0;
						else if (basePlaneChar == '2')
							vertDir = 1;
						else
							ok = false;
						if (ok)
							currentPoly->setMetaData(ccPolyline::MetaKeyUpDir(), QVariant(vertDir));
					}
					if (!ok)
					{
						ccLog::Warning(QString("[SinusX] Line %1 is corrupted (expected: 'CP base_plane')").arg(lineNumber));
						result = CC_FERR_MALFORMED_FILE;
						continue;
					}
				}
					++cpIndex;
					break;

				default:
					// ignored
					break;
				}
			}
			else if (!currentLine.isEmpty())
			{
				assert(currentVertices);

				// shoud be a point!
				QStringList tokens = currentLine.simplified().split(QChar(' '), QString::SkipEmptyParts);
				bool        ok     = (tokens.size() == 4);
				if (ok)
				{
					CCVector3d Pd;
					Pd.x = tokens[0].toDouble(&ok);
					if (ok)
					{
						Pd.y = tokens[1].toDouble(&ok);
						if (ok)
						{
							Pd.z = tokens[2].toDouble(&ok);
							if (ok)
							{
								// resize vertex cloud if necessary
								if (currentVertices->size() == currentVertices->capacity()
								    && !currentVertices->reserve(currentVertices->size() + 10))
								{
									delete currentPoly;
									return CC_FERR_NOT_ENOUGH_MEMORY;
								}
								// first point: check for 'big' coordinates
								if (firstVertex /*currentVertices->size() == 0*/)
								{
									firstVertex                  = false;
									bool preserveCoordinateShift = true;
									if (HandleGlobalShift(Pd, Pshift, preserveCoordinateShift, parameters))
									{
										if (preserveCoordinateShift)
										{
											if (currentPoly)
												currentPoly->setGlobalShift(Pshift);
											else
												currentVertices->setGlobalShift(Pshift);
										}
										ccLog::Warning("[SinusX::loadFile] Polyline has been recentered! Translation: (%.2f ; %.2f ; %.2f)", Pshift.x, Pshift.y, Pshift.z);
									}
								}

								currentVertices->addPoint((Pd + Pshift).toPC());
							}
						}
					}
				}
				if (!ok)
				{
					ccLog::Warning(QString("[SinusX] Line %1 is corrupted (expected: 'X Y Z Key ...')").arg(lineNumber));
					result = CC_FERR_MALFORMED_FILE;
					continue;
				}
			}
		}
	}

	// don't forget the last polyline!
	if (currentPoly)
	{
		if (currentVertices
		    && currentVertices->size() != 0
		    && currentVertices->resize(currentVertices->size())
		    && currentPoly->addPointIndex(0, currentVertices->size()))
		{
			container.addChild(currentPoly);
		}
	}

	return result;
}
